# -*- coding: utf8 -*-
import threading, random, time, sys
import pygame
from pygame.locals import *
from math import sin, cos, radians

# variables can and should be changed to create different circumstances
N = 5              # number of philosphers
COURSES = 5        # number of courses during a meal
THINKINGTIME = 1   # maximum time for thinking
EATINGTIME = 1     # maximum time for eating
PAUSE = 0.20       # reaction time after taking the left fork
SESSIONLIMIT = 20 #12. # time limit for the meal
MODE = 0           # defines if the improvement is used
VOLUME = 0.1

# The "improvement" shows the main problem while solving the philosophers'
# problem. This simple improvement succesfully prevents deadlock, but the
# process needs a lot more time, so mostly all philosophers will die of
# starvation! Therefore programmers normally have to strike a balance
# between preventing a deadlock and speed

# a few definitions for the printed output
THINK = ".T."
FULL = ".F."
WAIT = ".W."
LEFT = "|    "
RIGHT = "    |"
UNUSED = "  |  "

RUNNING = True
LAST=False

def wait(atmost=2):
    """Wait a random time period, default: maximum 2 seconds."""
    time.sleep(random.random() * atmost)

state = ({}, {}) # 1. dictionary: philosophers, 2. dictionary: forks
state_lock = threading.Lock()

start_lock = threading.Lock()
start_display_lock =threading.Lock()

class Philosopher(threading.Thread):
    def __init__(self, id):
        threading.Thread.__init__(self)
        self.id = id
        self.left_fork = forks[(id - 1) % N]
        self.right_fork = forks[id]
        self.feedings = 0
        self.setstate(THINK)
        # sets the type of the philosopher-thread to Daemon,
        # why this matters is described at the end
        self.setDaemon(True)  

    def setstate(self,status):
        state_lock.acquire()
        state[0][self.id] = status
        state_lock.release()

    def run(self):
        while (self.feedings < COURSES) and RUNNING:              
            self.think()
            self.acquire_forks()
            self.eat()
            self.release_forks()
        if self.feedings == COURSES:
            self.setstate(FULL)
            printstate("  - %d is full!" % self.id)
            
    def think(self):
        self.setstate(THINK)
        printstate("  - %d thinks." % self.id)
        wait(THINKINGTIME)

    def eat(self):
        self.feedings = self.feedings + 1
        self.setstate("%2d " % self.feedings) 
        printstate("  - %d eats." % self.id)
        wait(EATINGTIME)

    def acquire_forks(self):
        if MODE == 1:
            #If the Philosopher can't acquire the left fork,
            #he/she releases the left one, waits a bit
            #and restarts his/her action
            while True:
                self.left_fork.acquire(self.id)
                self.setstate(WAIT)
                printstate("  - %d waits." % self.id)
                time.sleep(PAUSE)
                if self.right_fork.owner == None:
                    self.right_fork.acquire(self.id)
                    break
                else:
                    self.left_fork.release()
                    time.sleep(PAUSE)
        else:
            self.left_fork.acquire(self.id)
            self.setstate(WAIT)
            printstate("  - %d waits." % self.id)
            time.sleep(PAUSE)
            self.right_fork.acquire(self.id)
                
        

    def release_forks(self):
        self.left_fork.release()
        self.right_fork.release()


class Fork(object):
    def __init__(self, i):
        self.lock = threading.Lock()
        self.id = i
        self.owner = None
        self.setstate(UNUSED)

    def setstate(self,status):
        state_lock.acquire()
        state[1][self.id] = status
        state_lock.release()

    def acquire(self, phid, blocking=True):
        if self.lock.acquire(blocking):
            self.owner = phid
            if self.id == phid:
                self.setstate(LEFT)
            else:
                self.setstate(RIGHT)
            printstate()
            return True
        printstate()
        return False

    def release(self, remark=""):
        self.lock.release()
        self.owner = None
        self.setstate(UNUSED)
        printstate(remark)

# ----------   OUTPUT-STUFF   -------------

def printstate(suff=""):
    if not RUNNING: return
    global last
    ts = timestamp() 
    s = " "
    state_lock.acquire()
    for i in range(N):
        s += state[0][i]
        s += state[1][i]
    if s.endswith("|"):
        s = "|" + s[1:-1] + " "
    if last != s:
        last = s
        print ts + s + suff
    state_lock.release()

# counts the time the programme runned till now
def timestamp():
    now = time.clock()
    return "%6.2f -- " % (now - starttime)

# looks at the situation at the end and decides whether
# a deadlock occured, someone starved to death or all
# philosophers have happily finished their meal
def cleanup():
    global RUNNING, LAST
    LAST=True
    RUNNING = False
    time.sleep(0.05) # wait a moment to finish ongoing printing
                     # if necessary
    print timestamp(), "SUM UP:\n"
    allwait = True
    for p in state[0]:
        if state[0][p] != WAIT:
            allwait = False
    if allwait:
        print "\n-------------------------"
        print timestamp(), "-- DEADLOCK!" 
        print "-------------------------\n"

    allfull = True
    for p in state[0]:
        if state[0][p] != FULL:
            print "%3d is starved to death!" % p
            allfull = False
    if allfull:
        print "Everyone is full!"
        
    print "\n******************** TERMINATION ********************"

def display(size, radius):
    global N, COURSES, THINKINGTIME, EATINGTIME, PAUSE, SESSIONLIMIT, MODE, LAST, VOLUME
    
    start_lock.acquire()
    start_display_lock.release()
    
    pygame.init()

    clock=pygame.time.Clock()
    
    screen = pygame.display.set_mode(size)
    background = pygame.Surface(size)
    backgroundimage=pygame.image.load("philosophers_background.jpg").convert()
    background.blit(backgroundimage,(0,0))
    
    font=pygame.font.Font(None,30)

    eat_sound_list = [pygame.mixer.Sound("eatloop1.ogg"), pygame.mixer.Sound("eatloop2.ogg"), pygame.mixer.Sound("eatloop3.ogg")]#
    theend_sound_list = [pygame.mixer.Sound("deadlock.ogg"), pygame.mixer.Sound("dead.ogg"), pygame.mixer.Sound("allefertig.ogg")]#
    soundplaying = False

    #----menu----

    globalvars={"N":N,"COURSES":COURSES,"THINKINGTIME":THINKINGTIME,"EATINGTIME":EATINGTIME,"PAUSE":PAUSE,"SESSIONLIMIT":SESSIONLIMIT,"MODE":MODE,"VOLUME":VOLUME}

    class Slider(object):
        def __init__(self,image,minx,maxx,y,name,varname,minvalue,maxvalue,step=0):
            self.image=image
            self.rect=self.image.get_rect()
            self.minx=minx
            self.maxx=maxx
            self.label=name+": "
            self.varname=varname
            self.minvalue=minvalue
            self.maxvalue=maxvalue
            self.rect.center=((maxx-minx)/2,y)
            self.step=step
            self.settovalue(globalvars[self.varname])

        def settovalue(self,value):
            self.value=value
            self.rect.center=((float(value)-self.minvalue)/(self.maxvalue-self.minvalue)*(self.maxx-self.minx)+self.minx,self.rect.center[1])

        def move(self,pos):
            if pos[0]>self.maxx:
                self.rect.center=(self.maxx,self.rect.center[1])
            
            elif pos[0]<self.minx:
                self.rect.center=(self.minx,self.rect.center[1])
            
            else:
                if self.step:
                    v=round((float(pos[0])-self.minx)/(self.maxx-self.minx)*(self.maxvalue-self.minvalue)+self.minvalue)
                    self.settovalue(v)
                else:
                    self.rect.center=(pos[0],self.rect.center[1])

            self.value=(float(self.rect.center[0])-self.minx)/(self.maxx-self.minx)*(self.maxvalue-self.minvalue)+self.minvalue
            globalvars[self.varname]=self.value
            
    bimage=pygame.image.load("startbutton.png").convert_alpha()
    brect=bimage.get_rect()
    brect.center=(size[0]/2,size[1]-20)

    #creating the sliders
    sliderimage=pygame.image.load("slider.png").convert_alpha()
    sliders=[]
    sliders.append(Slider(sliderimage,30,size[0]-30,50,"Number","N",3,8,step=1))
    sliders.append(Slider(sliderimage,30,size[0]-30,100,"Time","SESSIONLIMIT",1,60))
    sliders.append(Slider(sliderimage,30,size[0]-30,150,"Eatingtime","EATINGTIME",0,5))
    sliders.append(Slider(sliderimage,30,size[0]-30,200,"Thinkingtime","THINKINGTIME",0,5))
    sliders.append(Slider(sliderimage,30,size[0]-30,250,"Courses","COURSES",1,15,step=1))
    sliders.append(Slider(sliderimage,30,size[0]-30,300,"Pause","PAUSE",0,1))
    sliders.append(Slider(sliderimage,30,size[0]-30,350,"Mode","MODE",0,1,step=1))
    sliders.append(Slider(sliderimage,30,size[0]-30,400,"Volume","VOLUME",0,1))


    mslider=0

    while True:
        event=pygame.event.poll()
        if event.type == QUIT:
            break
        if event.type==MOUSEBUTTONDOWN:
            if brect.collidepoint(event.pos):
                START=True
                print "START!!"
                break
            for i in sliders:
                if i.rect.collidepoint(event.pos):
                    mslider=i
        elif event.type==MOUSEBUTTONUP:
            mslider=0
        elif event.type==MOUSEMOTION and mslider!=0:
            mslider.move(event.pos)
            

        screen.blit(background,(0,0))

        for i in sliders:
            if i.step==1:
                text=font.render(i.label+"%.0f"%(i.value,),1,(0,0,0))
            else:
                text=font.render(i.label+"%.2f"%(i.value,),1,(0,0,0))
            screen.blit(text,(size[0]/2,i.rect.y-25))

        
        for s in sliders:
            screen.blit(s.image,s.rect)

        screen.blit(bimage,brect)
        pygame.display.update()
        clock.tick(30)

    N=int(globalvars["N"])
    COURSES=globalvars["COURSES"]
    THINKINGTIME=globalvars["THINKINGTIME"]
    EATINGTIME=globalvars["EATINGTIME"]
    PAUSE=globalvars["PAUSE"]
    SESSIONLIMIT=globalvars["SESSIONLIMIT"]
    MODE=globalvars["MODE"]
    VOLUME=globalvars["VOLUME"]

    for element in eat_sound_list:
	element.set_volume(VOLUME)
    for element in theend_sound_list:
	element.set_volume(VOLUME)
    
    #---end of menu, start of main view---
    
    start_lock.release()
    start_display_lock.acquire()

    philimage_T=pygame.image.load("philot.png").convert_alpha()
    philimage_W=pygame.image.load("philow.png").convert_alpha()
    philimage_S=pygame.image.load("philos.png").convert_alpha()
    philimage_E=pygame.image.load("philoe.png").convert_alpha()
    philimage_V=pygame.image.load("philov.png").convert_alpha()
    tableimage=pygame.image.load("table.png").convert_alpha()
    forkimage=pygame.image.load("fork.png").convert_alpha()
    
    trect=tableimage.get_rect()
    trect.center=(size[0]/2,size[1]/2)
    background.blit(tableimage,trect)

    fradius=trect.width/2-20

    phrect=philimage_T.get_rect()
    forkrect=forkimage.get_rect()
    phrect.center=(size[0]/2,size[1]/2)
    forkrect.center=(size[0]/2,size[1]/2)
    philrects=[]
    forkrects=[]
    angle=0
    for pos in range(N):
        philrects.append(phrect.move(cos(radians(90-angle))*radius,-sin(radians(90-angle))*radius))
        forkrects.append(forkrect.move(cos(radians(90-(angle+360/N/2-360/N/4)))*fradius,-sin(radians(90-(angle+360/N/2-360/N/4)))*fradius))
        forkrects.append(forkrect.move(cos(radians(90-(angle+360/N/2)))*fradius,-sin(radians(90-(angle+360/N/2)))*fradius))
        forkrects.append(forkrect.move(cos(radians(90-(angle+360/N/2+360/N/4)))*fradius,-sin(radians(90-(angle+360/N/2+360/N/4)))*fradius))
        angle+=360/N

    while True:
        event=pygame.event.poll()
        if event.type == QUIT:
            sys.exit(0)

        if RUNNING or LAST:
            if LAST:
                LAST=2

            screen.blit(background,(0,0))
            
            state_lock.acquire()
            currentstate=state
            state_lock.release()

            if LAST:
                allwait = True
                for p in state[0]:
                    if state[0][p] != WAIT:
                        allwait = False
                if allwait:
                    theend_sound_list[0].play()
                else:
                    allfull = True
                    for p in state[0]:
                        if state[0][p] != FULL:
                            allfull = False
                            theend_sound_list[1].play()
                    if allfull:
                        theend_sound_list[2].play()
            
            currenttime=time.clock()-starttime
            if currenttime>SESSIONLIMIT: currenttime=SESSIONLIMIT
            
            for i in range(N):
                if LAST and not currentstate[0][i]==FULL:
                    image=philimage_V
                elif currentstate[0][i]==THINK:
                    image=philimage_T
                elif currentstate[0][i]==WAIT:
                    image=philimage_W
                    if soundplaying:
                        soundplaying = False
                elif currentstate[0][i]==FULL:
                    image=philimage_S
                else:
                    image=philimage_E
                    if not soundplaying:
                        random.choice(eat_sound_list).play(EATINGTIME)
                        soundplaying = True

                if currentstate[1][i]=="  |  ":
                    frect=forkrects[(i*3+1)%(N*3)]
                elif currentstate[1][i]=="|    ":
                    frect=forkrects[(i*3)%(N*3)]
                elif currentstate[1][i]=="    |":
                    frect=forkrects[(i*3+2)%(N*3)]
                    
                screen.blit(image,philrects[i])
                screen.blit(forkimage,frect)
            
            #for printing the time on the bottom of the window
            timetext=font.render("%.1fs/%.1fs"%(currenttime,SESSIONLIMIT),1,(0,0,0))
            timetextrect=timetext.get_rect()
            timetextrect.center=(size[0]/2,size[1]-30)
            screen.blit(timetext,timetextrect)
            
            pygame.display.update()
            if LAST==2:
                LAST=False
        clock.tick(10)
        
# ----------   main thread   -------------

start_display_lock.acquire()

view=threading.Thread(target=display,args=((600,600),175))
view.start()

start_display_lock.acquire()
start_lock.acquire()

    
# create forks:
last = ""
forks = []
for i in range(N):
    forks.append(Fork(i))

# create philosphers ...
philosophers = []
for i in range(N):
    p = Philosopher(i)
    philosophers.append(p)

starttime = time.clock()
# ... and bring them to live
for p in philosophers:
    p.start()   # 5 threads are started

start_display_lock.release()

# clean after SESSIONLIMIT
t = threading.Timer(SESSIONLIMIT, cleanup)
t.start()


# Here ends the main thread! After the SESSIONLIMIT
# when the timer-thread - which is no Daemon - has cleaned up,
# all " demonic " philosopher-threads will be terminated
# and the program ends

